define(function(require, exports, module) {
	
	var CodeMirror = require("../../codemirror");
	// TODO actually recognize syntax of TypeScript constructs

	CodeMirror.defineMode("javascript", function(config, parserConfig) {
		var indentUnit = config.indentUnit;
		var jsonMode = parserConfig.json;
		var isTS = parserConfig.typescript;

		// Tokenizer

		var keywords = function() {
			function kw(type) {
				return {
					type : type,
					style : "keyword"
				};
			}
			var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
			var operator = kw("operator"), atom = {
				type : "atom",
				style : "atom"
			};

			var jsKeywords = {
				"if" : A,
				"while" : A,
				"with" : A,
				"else" : B,
				"do" : B,
				"try" : B,
				"finally" : B,
				"return" : C,
				"break" : C,
				"continue" : C,
				"new" : C,
				"delete" : C,
				"throw" : C,
				"var" : kw("var"),
				"const" : kw("var"),
				"let" : kw("var"),
				"function" : kw("function"),
				"catch" : kw("catch"),
				"for" : kw("for"),
				"switch" : kw("switch"),
				"case" : kw("case"),
				"default" : kw("default"),
				"in" : operator,
				"typeof" : operator,
				"instanceof" : operator,
				"true" : atom,
				"false" : atom,
				"null" : atom,
				"undefined" : atom,
				"NaN" : atom,
				"Infinity" : atom
			};

			// Extend the 'normal' keywords with the TypeScript language
			// extensions
			if (isTS) {
				var type = {
					type : "variable",
					style : "variable-3"
				};
				var tsKeywords = {
					// object-like things
					"interface" : kw("interface"),
					"class" : kw("class"),
					"extends" : kw("extends"),
					"constructor" : kw("constructor"),

					// scope modifiers
					"public" : kw("public"),
					"private" : kw("private"),
					"protected" : kw("protected"),
					"static" : kw("static"),

					"super" : kw("super"),

					// types
					"string" : type,
					"number" : type,
					"bool" : type,
					"any" : type
				};

				for (var attr in tsKeywords) {
					jsKeywords[attr] = tsKeywords[attr];
				}
			}

			return jsKeywords;
		}();

		var isOperatorChar = /[+\-*&%=<>!?|]/;

		function chain(stream, state, f) {
			state.tokenize = f;
			return f(stream, state);
		}

		function nextUntilUnescaped(stream, end) {
			var escaped = false, next;
			while ((next = stream.next()) != null) {
				if (next == end && !escaped)
					return false;
				escaped = !escaped && next == "\\";
			}
			return escaped;
		}

		// Used as scratch variables to communicate multiple values without
		// consing up tons of objects.
		var type, content;
		function ret(tp, style, cont) {
			type = tp;
			content = cont;
			return style;
		}

		function jsTokenBase(stream, state) {
			var ch = stream.next();
			if (ch == '"' || ch == "'")
				return chain(stream, state, jsTokenString(ch));
			else if (/[\[\]{}\(\),;\:\.]/.test(ch))
				return ret(ch);
			else if (ch == "0" && stream.eat(/x/i)) {
				stream.eatWhile(/[\da-f]/i);
				return ret("number", "number");
			} else if (/\d/.test(ch) || ch == "-" && stream.eat(/\d/)) {
				stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
				return ret("number", "number");
			} else if (ch == "/") {
				if (stream.eat("*")) {
					return chain(stream, state, jsTokenComment);
				} else if (stream.eat("/")) {
					stream.skipToEnd();
					return ret("comment", "comment");
				} else if (state.lastType == "operator"
						|| state.lastType == "keyword c"
						|| /^[\[{}\(,;:]$/.test(state.lastType)) {
					nextUntilUnescaped(stream, "/");
					stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in
												// Mozilla
					return ret("regexp", "string-2");
				} else {
					stream.eatWhile(isOperatorChar);
					return ret("operator", null, stream.current());
				}
			} else if (ch == "#") {
				stream.skipToEnd();
				return ret("error", "error");
			} else if (isOperatorChar.test(ch)) {
				stream.eatWhile(isOperatorChar);
				return ret("operator", null, stream.current());
			} else {
				stream.eatWhile(/[\w\$_]/);
				var word = stream.current(), known = keywords
						.propertyIsEnumerable(word)
						&& keywords[word];
				return (known && state.lastType != ".") ? ret(known.type,
						known.style, word) : ret("variable", "variable", word);
			}
		}

		function jsTokenString(quote) {
			return function(stream, state) {
				if (!nextUntilUnescaped(stream, quote))
					state.tokenize = jsTokenBase;
				return ret("string", "string");
			};
		}

		function jsTokenComment(stream, state) {
			var maybeEnd = false, ch;
			while (ch = stream.next()) {
				if (ch == "/" && maybeEnd) {
					state.tokenize = jsTokenBase;
					break;
				}
				maybeEnd = (ch == "*");
			}
			return ret("comment", "comment");
		}

		// Parser

		var atomicTypes = {
			"atom" : true,
			"number" : true,
			"variable" : true,
			"string" : true,
			"regexp" : true
		};

		function JSLexical(indented, column, type, align, prev, info) {
			this.indented = indented;
			this.column = column;
			this.type = type;
			this.prev = prev;
			this.info = info;
			if (align != null)
				this.align = align;
		}

		function inScope(state, varname) {
			for (var v = state.localVars; v; v = v.next)
				if (v.name == varname)
					return true;
		}

		function parseJS(state, style, type, content, stream) {
			var cc = state.cc;
			// Communicate our context to the combinators.
			// (Less wasteful than consing up a hundred closures on every call.)
			cx.state = state;
			cx.stream = stream;
			cx.marked = null, cx.cc = cc;

			if (!state.lexical.hasOwnProperty("align"))
				state.lexical.align = true;

			while (true) {
				var combinator = cc.length ? cc.pop() : jsonMode
						? expression
						: statement;
				if (combinator(type, content)) {
					while (cc.length && cc[cc.length - 1].lex)
						cc.pop()();
					if (cx.marked)
						return cx.marked;
					if (type == "variable" && inScope(state, content))
						return "variable-2";
					return style;
				}
			}
		}

		// Combinator utils

		var cx = {
			state : null,
			column : null,
			marked : null,
			cc : null
		};
		function pass() {
			for (var i = arguments.length - 1; i >= 0; i--)
				cx.cc.push(arguments[i]);
		}
		function cont() {
			pass.apply(null, arguments);
			return true;
		}
		function register(varname) {
			var state = cx.state;
			if (state.context) {
				cx.marked = "def";
				for (var v = state.localVars; v; v = v.next)
					if (v.name == varname)
						return;
				state.localVars = {
					name : varname,
					next : state.localVars
				};
			}
		}

		// Combinators

		var defaultVars = {
			name : "this",
			next : {
				name : "arguments"
			}
		};
		function pushcontext() {
			cx.state.context = {
				prev : cx.state.context,
				vars : cx.state.localVars
			};
			cx.state.localVars = defaultVars;
		}
		function popcontext() {
			cx.state.localVars = cx.state.context.vars;
			cx.state.context = cx.state.context.prev;
		}
		function pushlex(type, info) {
			var result = function() {
				var state = cx.state;
				state.lexical = new JSLexical(state.indented, cx.stream
								.column(), type, null, state.lexical, info);
			};
			result.lex = true;
			return result;
		}
		function poplex() {
			var state = cx.state;
			if (state.lexical.prev) {
				if (state.lexical.type == ")")
					state.indented = state.lexical.indented;
				state.lexical = state.lexical.prev;
			}
		}
		poplex.lex = true;

		function expect(wanted) {
			return function expecting(type) {
				if (type == wanted)
					return cont();
				else if (wanted == ";")
					return pass();
				else
					return cont(arguments.callee);
			};
		}

		function statement(type) {
			if (type == "var")
				return cont(pushlex("vardef"), vardef1, expect(";"), poplex);
			if (type == "keyword a")
				return cont(pushlex("form"), expression, statement, poplex);
			if (type == "keyword b")
				return cont(pushlex("form"), statement, poplex);
			if (type == "{")
				return cont(pushlex("}"), block, poplex);
			if (type == ";")
				return cont();
			if (type == "function")
				return cont(functiondef);
			if (type == "for")
				return cont(pushlex("form"), expect("("), pushlex(")"),
						forspec1, expect(")"), poplex, statement, poplex);
			if (type == "variable")
				return cont(pushlex("stat"), maybelabel);
			if (type == "switch")
				return cont(pushlex("form"), expression,
						pushlex("}", "switch"), expect("{"), block, poplex,
						poplex);
			if (type == "case")
				return cont(expression, expect(":"));
			if (type == "default")
				return cont(expect(":"));
			if (type == "catch")
				return cont(pushlex("form"), pushcontext, expect("("), funarg,
						expect(")"), statement, poplex, popcontext);
			return pass(pushlex("stat"), expression, expect(";"), poplex);
		}
		function expression(type) {
			if (atomicTypes.hasOwnProperty(type))
				return cont(maybeoperator);
			if (type == "function")
				return cont(functiondef);
			if (type == "keyword c")
				return cont(maybeexpression);
			if (type == "(")
				return cont(pushlex(")"), maybeexpression, expect(")"), poplex,
						maybeoperator);
			if (type == "operator")
				return cont(expression);
			if (type == "[")
				return cont(pushlex("]"), commasep(expression, "]"), poplex,
						maybeoperator);
			if (type == "{")
				return cont(pushlex("}"), commasep(objprop, "}"), poplex,
						maybeoperator);
			return cont();
		}
		function maybeexpression(type) {
			if (type.match(/[;\}\)\],]/))
				return pass();
			return pass(expression);
		}

		function maybeoperator(type, value) {
			if (type == "operator" && /\+\+|--/.test(value))
				return cont(maybeoperator);
			if (type == "operator" && value == "?")
				return cont(expression, expect(":"), expression);
			if (type == ";")
				return;
			if (type == "(")
				return cont(pushlex(")"), commasep(expression, ")"), poplex,
						maybeoperator);
			if (type == ".")
				return cont(property, maybeoperator);
			if (type == "[")
				return cont(pushlex("]"), expression, expect("]"), poplex,
						maybeoperator);
		}
		function maybelabel(type) {
			if (type == ":")
				return cont(poplex, statement);
			return pass(maybeoperator, expect(";"), poplex);
		}
		function property(type) {
			if (type == "variable") {
				cx.marked = "property";
				return cont();
			}
		}
		function objprop(type) {
			if (type == "variable")
				cx.marked = "property";
			if (atomicTypes.hasOwnProperty(type))
				return cont(expect(":"), expression);
		}
		function commasep(what, end) {
			function proceed(type) {
				if (type == ",")
					return cont(what, proceed);
				if (type == end)
					return cont();
				return cont(expect(end));
			}
			return function commaSeparated(type) {
				if (type == end)
					return cont();
				else
					return pass(what, proceed);
			};
		}
		function block(type) {
			if (type == "}")
				return cont();
			return pass(statement, block);
		}
		function maybetype(type) {
			if (type == ":")
				return cont(typedef);
			return pass();
		}
		function typedef(type) {
			if (type == "variable") {
				cx.marked = "variable-3";
				return cont();
			}
			return pass();
		}
		function vardef1(type, value) {
			if (type == "variable") {
				register(value);
				return isTS ? cont(maybetype, vardef2) : cont(vardef2);
			}
			return pass();
		}
		function vardef2(type, value) {
			if (value == "=")
				return cont(expression, vardef2);
			if (type == ",")
				return cont(vardef1);
		}
		function forspec1(type) {
			if (type == "var")
				return cont(vardef1, expect(";"), forspec2);
			if (type == ";")
				return cont(forspec2);
			if (type == "variable")
				return cont(formaybein);
			return cont(forspec2);
		}
		function formaybein(type, value) {
			if (value == "in")
				return cont(expression);
			return cont(maybeoperator, forspec2);
		}
		function forspec2(type, value) {
			if (type == ";")
				return cont(forspec3);
			if (value == "in")
				return cont(expression);
			return cont(expression, expect(";"), forspec3);
		}
		function forspec3(type) {
			if (type != ")")
				cont(expression);
		}
		function functiondef(type, value) {
			if (type == "variable") {
				register(value);
				return cont(functiondef);
			}
			if (type == "(")
				return cont(pushlex(")"), pushcontext, commasep(funarg, ")"),
						poplex, statement, popcontext);
		}
		function funarg(type, value) {
			if (type == "variable") {
				register(value);
				return isTS ? cont(maybetype) : cont();
			}
		}

		// Interface

		return {
			startState : function(basecolumn) {
				return {
					tokenize : jsTokenBase,
					lastType : null,
					cc : [],
					lexical : new JSLexical((basecolumn || 0) - indentUnit, 0,
							"block", false),
					localVars : parserConfig.localVars,
					context : parserConfig.localVars && {
						vars : parserConfig.localVars
					},
					indented : 0
				};
			},

			token : function(stream, state) {
				if (stream.sol()) {
					if (!state.lexical.hasOwnProperty("align"))
						state.lexical.align = false;
					state.indented = stream.indentation();
				}
				if (stream.eatSpace())
					return null;
				var style = state.tokenize(stream, state);
				if (type == "comment")
					return style;
				state.lastType = type;
				return parseJS(state, style, type, content, stream);
			},

			indent : function(state, textAfter) {
				if (state.tokenize == jsTokenComment)
					return CodeMirror.Pass;
				if (state.tokenize != jsTokenBase)
					return 0;
				var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
				if (lexical.type == "stat" && firstChar == "}")
					lexical = lexical.prev;
				var type = lexical.type, closing = firstChar == type;
				if (type == "vardef")
					return lexical.indented
							+ (state.lastType == "operator"
									|| state.lastType == "," ? 4 : 0);
				else if (type == "form" && firstChar == "{")
					return lexical.indented;
				else if (type == "form")
					return lexical.indented + indentUnit;
				else if (type == "stat")
					return lexical.indented
							+ (state.lastType == "operator"
									|| state.lastType == "," ? indentUnit : 0);
				else if (lexical.info == "switch" && !closing)
					return lexical.indented
							+ (/^(?:case|default)\b/.test(textAfter)
									? indentUnit
									: 2 * indentUnit);
				else if (lexical.align)
					return lexical.column + (closing ? 0 : 1);
				else
					return lexical.indented + (closing ? 0 : indentUnit);
			},

			electricChars : ":{}"
		};
	});

	CodeMirror.defineMIME("text/javascript", "javascript");
	CodeMirror.defineMIME("application/json", {
				name : "javascript",
				json : true
			});
	CodeMirror.defineMIME("text/typescript", {
				name : "javascript",
				typescript : true
			});
	CodeMirror.defineMIME("application/typescript", {
				name : "javascript",
				typescript : true
			});

			
	module.exports = CodeMirror;		
});
